/*
 * Copyright (c) 2007 jMouse Gestures
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMouse Gestures' nor the names of its contributors 
 *   may be used to endorse or promote products derived from this software 
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.jmg.swing;

import java.awt.AWTEvent;
import java.awt.Component;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import javax.swing.SwingUtilities;
import javax.swing.JComponent;
import javax.swing.JPopupMenu;


/**
 * EventForwardingPane class forwards all events to the underlying 
 * <code>Components</code>.  The main purpose of this class is to be set as
 * a <code>JFrames</code> glass pane so that gestures can be easily performed
 * over the entire <code>JFrame</code>.
 * <p>
 * This class does't actually provide any new public methods as none are
 * required for it to forward events.
 * </p><p>
 * To set up this class as the glass pane use the following code:
 * <pre>
 * EventForwardingPane eventPane = new EventForwardingPane();
 *
 * YourJFrame.setGlassPane(eventPane);
 *
 * eventPane.setVisible(true);
 * </pre>
 * The glass pane needs to be visible before it will start receiving events.
 * </p>
 * 
 * @author  Christopher Martin
 * @version $Id: EventForwardingPane.java 13 2007-05-24 12:13:13Z GentlemanHal $
 */
public class EventForwardingPane extends JComponent {
    /** Serial Version UID */
    private static final long serialVersionUID = 1L;
    
    private transient Component previous;
    private transient JPopupMenu popupMenu;
    private transient boolean inDrag, wasPopUp;
    
    
    
    

    /** Creates a new instance of <code>EventForwardingPane</code>. */
    public EventForwardingPane() {
        super();
        
        enableEvents(AWTEvent.MOUSE_EVENT_MASK        | 
                     AWTEvent.MOUSE_MOTION_EVENT_MASK |
                     AWTEvent.MOUSE_WHEEL_EVENT_MASK);
        
        setOpaque(false);
    }
    
    
    

    
    /**
     * Forwards mouse events to the underlying components.
     *
     * @param  e The event to forward.
     * @throws java.lang.IllegalArgumentException If a <code>null</code> argument is
     *         passed while this <code>EventForwardingPane</code> is added
     *         to any <code>Container</code>.
     */
    protected void forwardEvent(MouseEvent e) 
        throws IllegalArgumentException
    {
        if (null == e) {
            throw new IllegalArgumentException("e was null!");
        }
        
        
        /*
         * Reasons to return:
         *
         * 1) No Layered Pane so no components to forward events to.
         * 2) Only the drag event needs to be sent.
         * 3) The release event only needs to be sent to the component
         *    the drag started on.
         */
        if (null == getTopLevelContainer() || checkForDragEvent(e) || checkForDragEnd(e)) {
            return;
        }
        
        
        Component component = getCorrectComponent(e);
        
        if (null != component) {
            checkForPopupMenu(e, component);
        
            checkForEnteredExitEvents(e, component);
            forwardOriginalEvent(e, component);

            setCursor(component.getCursor());
            
            previous = component;
        }
        
    }

    /**
     * Checks to see if the component is in a pop up menu.  This is required as
     * pop up menus require special handling.
     * 
     * @param e The mouse event.
     * @param component The current component;
     */
    private void checkForPopupMenu(MouseEvent e, Component component) {
        if (e.getID() == MouseEvent.MOUSE_CLICKED) {
            clearPreviousPopupMenu();
        } else {
            wasPopUp = findJPopupMenu(component.getParent());
        }
    }

    /**
     * Searches for an instance of <code>JPopupMenu</code>.  First this method
     * checks the component passed in, if this is not a <code>JPopupMenu</code>
     * instance it's hierarchy is checked.  This is done by repeatedly
     * calling <code>parent = parent.getParent();</code> and checking if the
     * returned component is a <code>JPopupMenu</code> instance.  This method
     * will terminate once the call to <code>getParent</code> returns
     * <code>null</code>.
     * <p>
     * This method is required to force pop up menus visible so that mouse
     * events forwarded to them can be handled correctly.
     * 
     * @param  parent The first component to check.
     * @return <code>true</code> if a <code>JPopupMenu</code> was found,
     *         <code>false</code> otherwise.
     */
    private boolean findJPopupMenu(Component parent) {
        while (null != parent) {           
            if (parent instanceof JPopupMenu) {
                clearPreviousPopupMenu();

                popupMenu = (JPopupMenu) parent;
                
                popupMenu.setVisible(true);
                
                return true;
            } else {
                parent = parent.getParent();
            }
        }
        
        return false;
        
    }

    /**
     * Hides and clears the reference to any previous <code>JPopupMenu</code>
     * instance found.
     * <p>
     * This method is required to hide the previous pop up menu as it was
     * manually made visible when it was found.
     * </p>
     *
     * @see #findJPopupMenu
     */
    private void clearPreviousPopupMenu() {
        if (null != popupMenu) {
            popupMenu.setVisible(false);
            popupMenu = null;
        }
    }
    
    /**
     * Checks to see if a mouse drag event needs to be forwarded.
     * 
     * @param  e The original mouse event.
     * @return <code>true</code> if a drag event was forwarded, 
     *         <code>false</code> otherwise.
     */
    private boolean checkForDragEvent(MouseEvent e) {
        if (e.getID() == MouseEvent.MOUSE_DRAGGED && null != previous) {
            
            previous.dispatchEvent(SwingUtilities.convertMouseEvent(this, e, previous));
            
            inDrag = true;
            return true;
            
        } else {
            return false;
        }
        
    }
    
    /**
     * Checks to see if a drag event has ended.
     * 
     * @param  e The original mouse event.
     * @return <code>true</code> if a drag event just ended, <code>false</code>
     *         otherwise.
     */
    private boolean checkForDragEnd(MouseEvent e) {
        if (e.getID() == MouseEvent.MOUSE_RELEASED && inDrag && null != previous) {    
            previous.dispatchEvent(SwingUtilities.convertMouseEvent(this, e, previous));    
            
            inDrag = false;
            return true;
            
        } else {
            return false;
        }
        
    }

    /**
     * Forwards the original mouse event to the given component.
     * 
     * @param e The event to forward.
     * @param component The component to forward too.
     */
    private void forwardOriginalEvent(MouseEvent e, Component component) {
        component.dispatchEvent(SwingUtilities.convertMouseEvent(this, e, component));
    }

    /**
     * Checks to see if entered/exited events need to be sent.
     * 
     * @param e The original mouse event.
     * @param current The component the original mouse event was on.
     */
    private void checkForEnteredExitEvents(MouseEvent e, Component current) {   
        if (previous != current) {

            if (null != previous) {
                previous.dispatchEvent(
                    new MouseEvent(
                        previous,
                        MouseEvent.MOUSE_EXITED, e.getWhen(), e.getModifiers(),
                        getPointOnComponent(e.getPoint(), current).x,
                        getPointOnComponent(e.getPoint(), current).y,
                        e.getClickCount(), e.isPopupTrigger()));
            }

            if (null != current) {
                current.dispatchEvent(
                    new MouseEvent(
                        current,
                        MouseEvent.MOUSE_ENTERED, e.getWhen(), e.getModifiers(),
                        getPointOnComponent(e.getPoint(), current).x,
                        getPointOnComponent(e.getPoint(), current).y,
                        e.getClickCount(), e.isPopupTrigger()));
            }
            
        }
        
    }
    
    /**
     * Returns the correct component for the event.  This is required as
     * special checking is required to forward events correctly to
     * pop-up menus.
     * 
     * @param  e The mouse event being processed.
     * @return The correct component for the event.
     */
    private Component getCorrectComponent(MouseEvent e) {
        if (wasPopUp && 
           (e.getID() == MouseEvent.MOUSE_PRESSED  || e.getID() == MouseEvent.MOUSE_RELEASED || 
            e.getID() == MouseEvent.MOUSE_CLICKED)) 
        {
            return previous;
            
        } else {
            return getDeepestComponentAt(e.getPoint()); 
        }
    }

    /**
     * Refer to 
     * {@link javax.swing.SwingUtilities#getDeepestComponentAt this} 
     * for a complete description of this method.
     * 
     * @param  p The point on the layered pane to check.
     * @return The component.
     */
    private Component getDeepestComponentAt(Point p) {
        return SwingUtilities.getDeepestComponentAt(
            getTopLevelContainer(), 
            getPointOnComponent(p, getTopLevelContainer()).x, 
            getPointOnComponent(p, getTopLevelContainer()).y
        ); 
    }
    
    /**
     * Converts the given point on this pane to a point on the given
     * component.
     * 
     * @param  pointOnThis The point on this glass pane.
     * @param  c The component to convert the point for.
     * @return The converted point.
     */
    private Point getPointOnComponent(Point pointOnThis, Component c) {
        SwingUtilities.convertPointToScreen(pointOnThis, this);
        SwingUtilities.convertPointFromScreen(pointOnThis, c);
        
        return pointOnThis;
    }

    /**
     * Returns the top level container or <code>null</code> if there is no 
     * top level container.  This method attempts to return the
     * <code>JLayeredPane</code> first as this will also allow pop up menus
     * to receive forwarded events.  If the <code>JLayeredPane</code> is
     * <code>null</code> then this method attempts to return the
     * <code>ContentPane</code>.  If this is also <code>null</code> then
     * <code>null</code> is returned.
     * 
     * @return The top level container or <code>null</code>.
     * @see    #getRootPane
     */
    private Component getTopLevelContainer() {
        Component c = null;
        
        if (null != getRootPane()) {
            if (null != getRootPane().getLayeredPane()) {
                c = getRootPane().getLayeredPane();
            } else if (null != getRootPane().getContentPane()) {
                c = getRootPane().getContentPane();
            }
        }
        
        return c;
    }

    
    
    
    
    /**
     * Processes mouse motion events, such as MouseEvent.MOUSE_DRAGGED.
     * 
     * @param e the <code>MouseEvent</code>
     * @see   MouseEvent
     */
    @Override
    protected void processMouseMotionEvent(MouseEvent e) {
        super.processMouseMotionEvent(e);
        
        if (!e.isConsumed()) {
            forwardEvent(e);
        }
    }

    /**
     * Processes mouse events occurring on this component by dispatching them to
     * any registered <code>MouseListener</code> objects, refer to
     * {@link java.awt.Component#processMouseEvent(MouseEvent)}
     * for a complete description of this method.
     * 
     * @param e the mouse event
     * @see   java.awt.Component#processMouseEvent
     */
    @Override
    protected void processMouseEvent(MouseEvent e) {
        super.processMouseEvent(e);
        
        if (!e.isConsumed()) {
            forwardEvent(e);
        }
    }

    /**
     * Processes mouse wheel events occurring on this component by dispatching
     * them to any registered <code>MouseWheelListener</code> objects.
     * 
     * @param e the mouse wheel event
     * @see   java.awt.event.MouseWheelEvent
     * @see   java.awt.event.MouseWheelListener
     */
    @Override
    protected void processMouseWheelEvent(MouseWheelEvent e) {
        super.processMouseWheelEvent(e);
        
        if (!e.isConsumed()) {
            forwardEvent(e);
        }
    }

}
